package edu.moravian.math;

import java.util.Random;

/**
 * This class represents a basic two-dimensional point. Methods that correspond
 * to common operations with vectors are supported.
 *
 * @author Elliot
 */
public class Point2D
{

    /**
     * This field is used as the tolerance for comparing two points for
     * equality.
     */
    public static final double TOL = 10e-8;
    
    private double x;
    private double y;

    /**
     * Create a new point representing the origin (0,0).
     */
    public Point2D()
    {
        this.x = 0.0;
        this.y = 0.0;
    }

    /**
     * Create a new point using the specified values.
     *
     * @param x the x value of the point
     * @param y the y value of the point
     */
    public Point2D(double x, double y)
    {
        this.x = x;
        this.y = y;
    }

    /**
     * Create a point with x and y values equal to the point passed in.
     *
     * @param p the point to "copy"
     */
    public Point2D(Point2D p)
    {
        this.x = p.x;
        this.y = p.y;
    }

    /**
     * Get the x component of the point.
     *
     * @return the value of x
     */
    public double getX()
    {
        return x;
    }

    /**
     * Get the y component of the point.
     *
     * @return the value of y
     */
    public double getY()
    {
        return y;
    }

    /**
     * Change the x component of the point.
     *
     * @param x the new value
     */
    public void setX(double x)
    {
        this.x = x;
    }

    /**
     * Change the y component of the point.
     *
     * @param y the new value
     */
    public void setY(double y)
    {
        this.y = y;
    }

    /**
     * Make this point have the same value as the parameter.
     *
     * @param p the point to "copy"
     */
    public void set(Point2D p)
    {
        this.x = p.x;
        this.y = p.y;
    }

    /**
     * Computer the vector representing this - rhs.
     *
     * @param rhs the right hand side of the difference
     *
     * @return the vector point from rhs to this
     */
    public Vector2D minus(Point2D rhs)
    {
        double xVec = x - rhs.x;
        double yVec = y - rhs.y;

        return new Vector2D(xVec, yVec);
    }

    /**
     * Add the vector to this point: this = this + rhs.
     *
     * @param rhs the vector to add
     */
    public void plusEquals(Vector2D rhs)
    {
        x += rhs.getX();
        y += rhs.getY();
    }

    /**
     * Compute the sum of this point and the vector parameter. The point will
     * not be modified: return this + rhs
     *
     * @param rhs the vector to add
     *
     * @return the sum of the point and the vector
     */
    public Point2D plus(Vector2D rhs)
    {
        double newX = x + rhs.getX();
        double newY = y + rhs.getY();

        return new Point2D(newX, newY);
    }

    /**
     * Modify this point by adding the scaled version of the vector: this = this
     * + scalar * v.
     *
     * @param scalar the scalar
     * @param v the vector
     */
    public void scalePlusEquals(double scalar, Vector2D v)
    {
        x += (v.getX() * scalar);
        y += (v.getY() * scalar);
    }

    /**
     * Compute the addition of this point and the scaled vector. This point is
     * not changed by this operation: return this + scalar * v
     *
     * @param scalar the scalar
     * @param v the vector
     *
     * @return the sum of this point and the scaled vector.
     */
    public Point2D scalePlus(double scalar, Vector2D v)
    {
        double newX = x + (v.getX() * scalar);
        double newY = y + (v.getY() * scalar);

        return new Point2D(newX, newY);
    }

    /**
     * Ensure that the point is within the range 0 <= x < xMax and 0 <= y <
     * yMax. If it isn't, wrap the point as though it is on a torus.
     *

     *
     * @param xMax the upper bound on the x value
     * @param yMax the upper bound on the y value
     */
    public void modEquals(double xMax, double yMax)
    {

        if (xMax <= 0 || yMax <= 0)
        {
            throw new IllegalArgumentException("xMax and yMax must be positive numbers");
        }

        while (x >= xMax)
        {
            x -= xMax;
        }
        while (x < 0)
        {
            x += xMax;
        }

        while (y >= yMax)
        {
            y -= yMax;
        }
        while (y < 0)
        {
            y += yMax;
        }
    }

    /**
     * Computes the distance between this point and the point passed in
     *
     * @param that the point to check the distance to
     *
     * @return the distance between the points
     */
    public double distance(Point2D that)
    {
        double xDist = this.getX() - that.getX();
        double yDist = this.getY() - that.getY();

        double dist = Math.sqrt(xDist * xDist + yDist * yDist);

        return dist;
    }

    /**
     * Computes the distance squared between this point and the point passed in
     *
     * @param that the point to check the distanceSq to
     *
     * @return the distanceSq between the points
     */
    public double distanceSq(Point2D that)
    {
        double xDist = this.getX() - that.getX();
        double yDist = this.getY() - that.getY();

        double distSq = xDist * xDist + yDist * yDist;

        return distSq;
    }

    /**
     * Two points are equal if their x and y components are both within TOL of
     * each other.
     *
     * NOTE: this violates the contract of equals in object -- we violate the
     * transitive property
     *
     * @param aThat the object to compare against
     *
     * @return true if they are equal, false otherwise
     */
    @Override
    public boolean equals(Object aThat)
    {
        if (this == aThat)
        {
            return true;
        }

        if (aThat instanceof Point2D == false)
        {
            return false;
        }

        Point2D that = (Point2D)aThat;

        double xDiff = Math.abs(this.x - that.x);
        double yDiff = Math.abs(this.y - that.y);

        if (xDiff < TOL && yDiff < TOL)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    /**
     * Compute the hash code for this point.
     *
     * NOTE: Because equals violates object's contract, hashCode does as well
     * because two "equal" objects will have different hashes
     *
     * @return the hash code
     */
    @Override
    public int hashCode()
    {
        int hash = 7;
        hash = 23 * hash + (int)(Double.doubleToLongBits(this.x) ^ (Double.doubleToLongBits(this.x) >>> 32));
        hash = 23 * hash + (int)(Double.doubleToLongBits(this.y) ^ (Double.doubleToLongBits(this.y) >>> 32));
        return hash;
    }

    /**
     * Convert this point to a string of the form "(x, y)".
     *
     * @return the string representation of this point
     */
    @Override
    public String toString()
    {
        //there are a couple ways this could be done such as formatting 
        //how many decimals are displayed or using Double.toString() but 
        //this should suffice for a simple toString()
        return "(" + x + ", " + y + ")";
    }

    /**
     * Create a random point in the range [xMin, xMax) and [yMin, yMax).
     *
     * @param xMin the (inclusive) minimum for x.
     * @param xMax the (exclusive) maximum for x.
     * @param yMin the (inclusive) minimum for y.
     * @param yMax the (exclusive) maximum for y.
     *
     * @return the random point ([xMin, xMax), [yMin, yMax))
     */
    public static Point2D randomPoint(double xMin, double xMax, double yMin, double yMax)
    {
        Random random = new Random();

        double randX = xMin + random.nextDouble() * (xMax - xMin);
        double randY = yMin + random.nextDouble() * (yMax - yMin);

        return new Point2D(randX, randY);
    }
}
